【Unity URP】Unity Procedural Skybox Shader

Posted by FlowingCrescent on 2022-01-20
Estimated Reading Time 19 Minutes
Words 4.2k In Total
Viewed Times

前言

Github:https://github.com/FlowingCrescent/SimpleProceduralSkybox_URP

虽然我已经写过不少Shader,但程序化Skybox相关的Shader至今还未接触,因此这次先从Unity自带的Procedural Skybox入手研究,打算之后写个自己的风格化Skybox。

Unity的自带程序化天空盒

unity skybox.gif
Unity Procedural Skybox

Unity自带的程序化Skybox Github

如何在天空盒上画一个简单的太阳

这个shader非常神奇,绝大部分计算都在vert阶段进行。
当材质的Sun设置为Simple时,天空盒便不会有大气散射效果,太阳就只是一个圆,就先从这最简单的部分开始理解吧。

1
2
3
4
5
6
7
8
#if SKYBOX_SUNDISK != SKYBOX_SUNDISK_NONE
if (y < 0.0)
{
col += IN.sunColor * calcSunAttenuation(_WorldSpaceLightPos0.xyz, -ray);
}
#endif

return half4(col, 1.0);

frag中的这一部分绘制了太阳

若我们将核心的那行代码改成
col = 10 * calcSunAttenuation(_WorldSpaceLightPos0.xyz, -ray);
效果如图:
image.png
那么显然,本来的IN.sunColor只是输入了一个颜色值,最重要的太阳绘制就是
calcSunAttenuation(_WorldSpaceLightPos0.xyz, -ray)负责。
那么分析一下输入的参数
_WorldSpaceLightPos0.xyz,如果是直接光,这xyz便是直接光的世界空间光照方向,也就是太阳光线照向我们的方向。

而ray则是从vertex中传入的值
in vertex:

1
2
3
4
5
6
7
// Get the ray from the camera to the vertex and its length (which is the far point of the ray passing through the atmosphere)
float3 eyeRay = normalize(mul((float3x3)unity_ObjectToWorld, v.vertex.xyz));

//......

#elif SKYBOX_SUNDISK == SKYBOX_SUNDISK_SIMPLE
OUT.rayDir = half3(-eyeRay);

eyeRay是从世界原点指向天空盒顶点,而rayDir则方向相反。

那么让我们仔细看看calSunAttenuation这个函数,只看SKYBOX_SUNDISK_SIMPLE的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
// Calculates the sun shape
half calcSunAttenuation(half3 lightPos, half3 ray)
{
#if SKYBOX_SUNDISK == SKYBOX_SUNDISK_SIMPLE
half3 delta = lightPos - ray;
half dist = length(delta);
half spot = 1.0 - smoothstep(0.0, _SunSize, dist);
return spot * spot;
#else // SKYBOX_SUNDISK_HQ
half focusedEyeCos = pow(saturate(dot(lightPos, ray)), _SunSizeConvergence);
return getMiePhase(-focusedEyeCos, focusedEyeCos * focusedEyeCos);
#endif
}

其实有了这俩向量之后,无非就是看俩向量之间的“距离”相差多少了,还是比较简单的。

天空与地面颜色的计算

在vertex shader中,使用eyeRay.y的正负来区分是天空还是地面
天空部分的颜色计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
if (eyeRay.y >= 0.0)
{
// Sky
// Calculate the length of the "atmosphere"
//比较神秘的一个计算,计算结果为地平线开始到某一仰角,计算结果从小到大,而之后随仰角增加而递减
far = sqrt(kOuterRadius2 + kInnerRadius2 * eyeRay.y * eyeRay.y - kInnerRadius2) - kInnerRadius * eyeRay.y;
float3 pos = cameraPos + far * eyeRay;

// Calculate the ray's starting position, then calculate its scattering offset
float height = kInnerRadius + kCameraHeight;
float depth = exp(kScaleOverScaleDepth * (-kCameraHeight));
float startAngle = dot(eyeRay, cameraPos) / height;
float startOffset = depth * scale(startAngle);


// Initialize the scattering loop variables
float sampleLength = far / kSamples;
float scaledLength = sampleLength * kScale;
float3 sampleRay = eyeRay * sampleLength;
float3 samplePoint = cameraPos + sampleRay * 0.5;

// Now loop through the sample rays
float3 frontColor = float3(0.0, 0.0, 0.0);
for(int i=0; i<int(kSamples); i++)
{
float height = length(samplePoint);
float depth = exp(kScaleOverScaleDepth * (kInnerRadius - height));
float lightAngle = dot(_WorldSpaceLightPos0.xyz, samplePoint) / height;
float cameraAngle = dot(eyeRay, samplePoint) / height;
float scatter = (startOffset +depth * (scale(lightAngle) - scale(cameraAngle)));
float3 attenuate = exp(-clamp(scatter, 0.0, kMAX_SCATTER) * (kInvWavelength * kKr4PI + kKm4PI));

frontColor += attenuate * (depth * scaledLength);
samplePoint += sampleRay;
}

// Finally, scale the Mie and Rayleigh colors and set up the varying variables for the pixel shader
cIn = frontColor * (kInvWavelength * kKrESun);
cOut = frontColor * kKmESun;
}

大概是因为这些计算比较基于物理,直接看通常看不懂在干什么,我也只能大致看看每个参数的值了,
far的计算结果如图:
image.png

depth的计算结果如图:
image.png

startOffset的计算结果如图:
image.png
之后的计算可以说非常看不懂了,也就作罢,已经知道大概的流程了。

自定义风格化天空盒

先参考这一篇文章:Reaching for the stars 试试做个大概。

这文用的是Shader Graph,而我当然还是要手写Shader,于是第一个小坑就来了
如果Pass中填了LightMode,如:
Tags { "LightMode"="UniversalForward" }
那么天空盒将会没效果,将这行注释掉就正常了。
也不能填SRPDefaultUnlit,虽然按理来说不填LightMode会默认是这个Mode。
关于URP的LightMode官方文档

重建天空盒的UV

如果使用天空盒原本的uv,采样这张uv check图的话,结果会非常悲剧
checker-map_tho.png
image.png

于是将世界坐标从三维笛卡尔坐标系转换到球坐标系来作为uv。
其中atan2第一次见到,看了下wiki,原来就是二维笛卡尔坐标系中输入一个点的坐标,它输出对应的弧度值。
atan的范围在(-π, π],因此将其除以2π后映射到(-0.5, 0.5].
而asin(1)的结果为π/2,将返回值除以π/2,最终结果会映射到[-1, 1],与方位角的映射值不一样是为了之后做渐变时方便一些。

1
2
float3 normalizePosWS = normalize(i.positionWS);
float2 sphereUV = float2(atan2(normalizePosWS.x, normalizePosWS.z)/PI2,asin(normalizePosWS.y)/PI);

image.png
现在用这计算出的uv来采样贴图,效果就还可以了。

天空颜色渐变

直接用之前的uv.y来做文章就可以了

1
half4 skyColor = lerp(_HorizonColor, _SkyColor, smoothstep(-0.1, _GradientLength, sphereUV.y));

image.png
只不过现在渐变比较难以调整,还是用代码生成ramp图来取值吧。

新建了一个SkyController类,打算之后都用这个脚本进行材质的调整和其他相关数据的控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class SkyController : MonoBehaviour
{
public Gradient daytimeSkyGradient;
public Gradient nightSkyGradient;

public int resolution = 128;
public Material skyboxMat;

private const string _DaytimeSkyGradientTex = "_DaytimeSkyGradientTex";
private const string _SunDirection = "_SunDirection";

void Update()
{
GenerateSkyGradientColorToShader(daytimeSkyGradient, _DaytimeSkyGradientTex);
}


public void GenerateSkyGradientColorToShader(Gradient gradient, string texName)
{
Texture2D tex = new Texture2D(resolution, 1, TextureFormat.ARGB32, false, true);
tex.filterMode = FilterMode.Bilinear;
tex.wrapMode = TextureWrapMode.Clamp;


for(int i = 0; i < resolution; i++)
{
tex.SetPixel(i, 0, gradient.Evaluate(i * 1.0f / resolution).linear);
}
tex.Apply(false, false);

skyboxMat.SetTexture(texName, tex);
}


}

代码很简单,就是用Unity自带的Gradient生成渐变图传入材质,注意的是这时候没生成贴图资产,也不需要伽马校正,因此传入的是Linear值,其他就不多解释了。
image.png

绘制太阳

这时候就使用Unity自带的方法绘制太阳,Unity直接取用了LightDirection,但实际上之后做昼夜切换的话晚上也是要有(从上往下照明的)直接光的,但是晚上总不能有太阳吧。
看了下Azure Sky的实现,发现它是用脚本根据昼夜时间来操作空物体的Transform,比如白天的时候空物体和光的旋转相同,晚上则另行计算,再传入空物体的Transform.forward方向给材质,那确实将太阳与光照方向解耦了。

直接抄Unity的那个太阳计算就行了,而且既然抄就抄高级版,有米氏散射的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
        // Calculates the Mie phase function
half getMiePhase(half eyeCos, half eyeCos2)
{
half temp = 1.0 + MIE_G2 - 2.0 * MIE_G * eyeCos;
temp = pow(temp, pow(_SunSize, 0.65) * 10);
temp = max(temp, 1.0e-4); // prevent division by zero, esp. in half precision
temp = 1.5 * ((1.0 - MIE_G2) / (2.0 + MIE_G2)) * (1.0 + eyeCos2) / temp;

return temp;
}

// Calculates the sun shape
half calcSunAttenuation(half3 lightPos, half3 ray)
{
// half3 delta = lightPos - ray;
// half dist = length(delta);
// half spot = 1.0 - smoothstep(0.0, _SunSize, dist);
// return spot * spot;

half focusedEyeCos = pow(saturate(dot(lightPos, ray)), 5);
return getMiePhase(-focusedEyeCos, focusedEyeCos * focusedEyeCos);
}

half4 frag(v2f i) : SV_Target {

float3 normalizePosWS = normalize(i.positionWS);
float2 sphereUV = float2(atan2(normalizePosWS.x, normalizePosWS.z)/PI2,asin(normalizePosWS.y)/halfPI);


half4 dayTimeSkyCol = SAMPLE_TEXTURE2D(_DaytimeSkyGradientTex, sampler_DaytimeSkyGradientTex, float2(sphereUV.y, 0.5));

//抄Unity自带的太阳计算
half4 sun = calcSunAttenuation(normalizePosWS, -_SunDirectionWS) * _SunIntensity * _SunCol;
//自定义的一个类似大范围bloom的散射
half scattering = smoothstep(0.5, 1.5, dot(normalizePosWS, -_SunDirectionWS)) * 0.15 * _SunCol;

half4 finCol = dayTimeSkyCol + sun + scattering;

return finCol;
}


效果如图:
image.png

然后先随便做一下根据太阳角度而进行颜色过渡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
half4 frag(v2f i) : SV_Target {

float3 normalizePosWS = normalize(i.positionWS);
float2 sphereUV = float2(atan2(normalizePosWS.x, normalizePosWS.z)/PI2,asin(normalizePosWS.y)/halfPI);


half4 dayTimeSkyCol = SAMPLE_TEXTURE2D(_DaytimeSkyGradientTex, sampler_DaytimeSkyGradientTex, float2(sphereUV.y, 0.5));

half4 dawnSkyCol = SAMPLE_TEXTURE2D(_DawnSkyGradientTex, sampler_DawnSkyGradientTex, float2(sphereUV.y, 0.5));


//抄Unity自带的太阳计算
half4 sun = calcSunAttenuation(normalizePosWS, -_SunDirectionWS) * _SunIntensity * _SunCol;
//自定义的一个类似大范围bloom的散射
half4 scattering = smoothstep(0.5, 1.5, dot(normalizePosWS, -_SunDirectionWS)) * _SunCol;
//刚日出时散射强度大
half sactteringIntensity = max(0.15, smoothstep(0.6, 0.0, -_SunDirectionWS.y));
scattering *= sactteringIntensity;

//日出颜色与白天颜色插值
half4 skyColor = lerp(dawnSkyCol, dayTimeSkyCol, smoothstep(0.0, 0.6, -_SunDirectionWS.y));

half4 finCol = skyColor + sun + scattering;

return finCol;
}

但发现Unity不支持实时Bake GI……之后大概只能使用自带的Gradient Amibient Light了。
mySkybox_1127.gif
QQ图片20211127183129.png
QQ图片20211127183135.png
QQ图片20211127183138.png

绘制星空

那么终于来到了夜晚天空盒的部分
参考文章中是在shader中实时计算Voronoi Noise,于是我也试了试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
         inline float2 VoronoiRandomVector(float2 UV, float offset)
{
float2x2 m = float2x2(15.27, 47.63, 99.41, 89.98);
UV = frac(sin(mul(UV, m)) * 46839.32);
return float2(sin(UV.y*+offset)*0.5+0.5, cos(UV.x*offset)*0.5+0.5);
}

void VoronoiNoise(float2 UV, float AngleOffset, float CellDensity, out float Out, out float Cells)
{
float2 g = floor(UV * CellDensity);
float2 f = frac(UV * CellDensity);
float t = 8.0;
float3 res = float3(8.0, 0.0, 0.0);

for(int y=-1; y<=1; y++)
{
for(int x=-1; x<=1; x++)
{
float2 lattice = float2(x,y);
float2 offset = VoronoiRandomVector(lattice + g, AngleOffset);
float d = distance(lattice + offset, f);
if(d < res.x)
{
res = float3(d, offset.x, offset.y);
Out = res.x;
Cells = res.y;
}
}
}
}


half4 frag(v2f i) : SV_Target {


//......

half star;
half cell;
VoronoiNoise(sphereUV, 20, 100, star, cell);

star = pow(1 - saturate(star), 200);
return star;

}

image.png
静帧看效果还不错,但是镜头移动时会发生比较剧烈的闪烁。
推测是由于我们输入的uv也是实时计算获得,浮点数精度不足导致voronoi noise返回的值产生些微变化,而之后的pow操作更是放大了这一误差。

那么就别实时算Voronoi了,这星空咱们直接得个图出来不就好了。
打开SD,如图直接用自带的Cells(也就是Voronoi)加上curve整出个星空来,然后pow给个差不多的值就行。
image.png

Stars.png
于是直接采样图片后做些微调整即可

1
2
star = SAMPLE_TEXTURE2D(_StarTex, sampler_StarTex, sphereUV);
star = saturate(star * star * star * 3);

image.png
倒是现在也还会有极轻微的闪烁情况,歪打正着可以当星星闪烁的效果了……

绘制月亮

月亮想必是无法使用像太阳那样的方法直接算了,我们必须采样图片
但这个采样用的uv怎么获得?

image.png
其实道理也很简单,就是把天空盒的坐标变换到光源的坐标系,然后取xy值就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// in C#
skyboxMat.SetMatrix(_MoonWorld2Local, sunTransform.worldToLocalMatrix);


// in vertex shader
o.moonPos = mul((float3x3)_MoonWorld2Obj, v.positionOS.xyz) * 6;
o.moonPos.x *= -1;


// in frag shader
half4 moon = SAMPLE_TEXTURE2D(_MoonTex, sampler_MoonTex, (i.moonPos.xy + 0.5));
half4 moonScattering = smoothstep(0.97, 1.3, dot(normalizePosWS, -_SunDirectionWS));
moon = (moon * _MoonIntensity + moonScattering * 0.8)* _MoonCol;
return skyColor + star + moon;

效果如图:
image.png
要注意的是我们这里使用天空盒的模型空间坐标来进行转换,因为世界空间坐标(如下图TEXCOORD1)的值过大,要用于转换也需要先normalize。不过由此来看,模型空间坐标与世界空间坐标之间的关系近似于进行了一个等比缩放,按理来说我们的天空盒uv计算其实也可以使用模型空间坐标。
image.png

绘制银河

银河就比较难以制作了
在游戏的程序化生成银河中原神的这个是相当值得参考的:
image.png
如果观看视频或实际游玩可以很容易地观察出,其实就是两个相反方向不同颜色的noise在流动,而大体的形状应当是算出来的,而这个形状八成也是依靠Noise相加/减之类的操作获得。

于是随手用PS的渐变、涂抹工具做了个图
image.png
R通道储存银河大体的形状
image.png
G通道储存比较中心的值,用于相减Noise时调整Noise的强度
image.png

B通道跟R通道的值一样,我没做什么调整。
于是一通疯狂调参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
half4 milkyWayTex = SAMPLE_TEXTURE2D(_MilkyWayTex, sampler_MilkyWayTex, (i.milkyWayPos.xy + 0.5));
half milkyWay = smoothstep(0, 0.7, milkyWayTex.r);


half noiseMove1 = SAMPLE_TEXTURE2D(_MilkyWayNoise, sampler_MilkyWayNoise, (i.milkyWayPos.xy + 0.5) * _MilkyWayNoise_ST.xy + _MilkyWayNoise_ST.zw + float2(0, _Time.y * _FlowSpeed));
half noiseMove2 = SAMPLE_TEXTURE2D(_MilkyWayNoise, sampler_MilkyWayNoise, (i.milkyWayPos.xy + 0.5) * _MilkyWayNoise_ST.xy - _MilkyWayNoise_ST.zw - float2(0, _Time.y * _FlowSpeed));
half noiseStatic = SAMPLE_TEXTURE2D(_MilkyWayNoise, sampler_MilkyWayNoise, (i.milkyWayPos.xy + 0.5) * _MilkyWayNoise_ST.xy * 0.5);

milkyWay *= smoothstep(-0.2, 0.8, noiseStatic + milkyWay);
milkyWay *= smoothstep(-0.4, 0.8, noiseStatic);

noiseMove1 = smoothstep(0.0, 1.2, noiseMove1);

half milkyWay1 = milkyWay;
half milkyWay2 = milkyWay;
milkyWay1 -= noiseMove1 * (smoothstep(0.4, 1, milkyWayTex.g) + 0.4);
milkyWay2 -= noiseMove2 * (smoothstep(0.4, 1, milkyWayTex.g) + 0.4);

milkyWay1 = saturate(milkyWay1);
milkyWay2 = saturate(milkyWay2);

half3 milkyWayCol1 = milkyWay1 * _MilkyWayCol1.rgb * _MilkyWayCol1.a;
half3 milkyWayCol2 = milkyWay2 * _MilkyWayCol2.rgb * _MilkyWayCol2.a;

half milkyStar;
half cell;
VoronoiNoise(sphereUV, 20, 200, milkyStar, cell);

milkyStar = pow(1 - saturate(milkyStar), 50) * (smoothstep(0.2, 1, milkyWayTex.g) + milkyWayTex.r * 0.5) * 3 * _MilkywayIntensity;


half3 milkywayBG = smoothstep(0.1, 1.5, milkyWayTex.r) * _MilkyWayCol1.rgb * 0.2;

half3 milkyCol = (softLight(milkyWayCol1, milkyWayCol2) + softLight(milkyWayCol2, milkyWayCol1)) * 0.5 * _MilkywayIntensity + milkywayBG;

return skyColor + star + moon + milkyCol.rgbr + milkyStar;

image.png
也就达到勉强能看的水平,跟原神还差得很远,不知是算法的原因还是素材的原因。

数据管理

到现在为止还都是一个图一个图去插值,也没法做昼夜变换(用光源角度来进行插值也不是不行,就是有点蠢,也很难做多个时间点的变化),于是差不多该做一下天空盒的数据管理了。

用一个ScriptableObject存储不同时间段的天空盒数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName ="Skybox/SkyTimeData")]
public class SkyTimeData : ScriptableObject
{
public Gradient skyColorGradient;
public float scatteringIntensity;
public float starIntensity;
public float milkywayIntensity;


[HideInInspector]
public Texture2D skyColorGradientTex;
}

然后新开一个类来管理这些数据,达到输入一个时间就可以获取这个时间的天空盒数据的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class SkyTimeDataController : MonoBehaviour
{
[System.Serializable]
public class SkyTimeDataCollection
{
public SkyTimeData time0;
public SkyTimeData time3;
public SkyTimeData time6;
public SkyTimeData time9;
public SkyTimeData time12;
public SkyTimeData time15;
public SkyTimeData time18;
public SkyTimeData time21;
}
private SkyTimeData newData = ScriptableObject.CreateInstance("SkyTimeData") as SkyTimeData;
public SkyTimeDataCollection skyTimeDataCollection = new SkyTimeDataCollection();

private void OnEnable() {
newData = ScriptableObject.CreateInstance("SkyTimeData") as SkyTimeData;
}

public SkyTimeData GetSkyTimeData(float time)
{
SkyTimeData start = skyTimeDataCollection.time0;
SkyTimeData end = skyTimeDataCollection.time0;

if(time >= 0 && time < 3)
{
start = skyTimeDataCollection.time0;
end = skyTimeDataCollection.time3;
}
else if(time >= 3 && time < 6)
{
start = skyTimeDataCollection.time3;
end = skyTimeDataCollection.time6;
}
else if(time >= 6 && time < 9)
{
start = skyTimeDataCollection.time6;
end = skyTimeDataCollection.time9;
}
else if(time >= 9 && time < 12)
{
start = skyTimeDataCollection.time9;
end = skyTimeDataCollection.time12;
}
else if(time >= 12 && time < 15)
{
start = skyTimeDataCollection.time12;
end = skyTimeDataCollection.time15;
}
else if(time >= 15 && time < 18)
{
start = skyTimeDataCollection.time15;
end = skyTimeDataCollection.time18;
}
else if(time >= 18 && time < 21)
{
start = skyTimeDataCollection.time18;
end = skyTimeDataCollection.time21;
}
else if(time >= 21 && time < 24)
{
start = skyTimeDataCollection.time21;
end = skyTimeDataCollection.time0;
}

float lerpValue = (time % 3 / 3f);
newData.skyColorGradientTex = GenerateSkyGradientColorTex(start.skyColorGradient, end.skyColorGradient, 128, lerpValue);

newData.starIntensity = Mathf.Lerp(start.starIntensity, end.starIntensity, lerpValue);
newData.milkywayIntensity = Mathf.Lerp(start.milkywayIntensity, end.milkywayIntensity, lerpValue);


return newData;
}



public Texture2D GenerateSkyGradientColorTex(Gradient startGradient, Gradient endGradient, int resolution, float lerpValue)
{
Texture2D tex = new Texture2D(resolution, 1, TextureFormat.RGBAFloat, false, true);
tex.filterMode = FilterMode.Bilinear;
tex.wrapMode = TextureWrapMode.Clamp;


for(int i = 0; i < resolution; i++)
{
Color start = startGradient.Evaluate(i * 1.0f / resolution).linear;
Color end = endGradient.Evaluate(i * 1.0f / resolution).linear;

Color fin = Color.Lerp(start, end, lerpValue);

tex.SetPixel(i, 0, fin);
}
tex.Apply(false, false);

return tex;
}


}

这时候SkyController就干净很多了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class SkyController : MonoBehaviour
{
[Range(0f, 24f)]
public float time = 9f;
public SkyTimeDataController skyTimeDataController;

public Texture2D starTex;
public Texture2D moonTex;
public Light mainLight;
public Transform sunTransform;
public Transform milkyWayTransform;
public Material skyboxMat;


private const string _SkyGradientTex = "_SkyGradientTex";
private const string _StarIntensity = "_StarIntensity";
private const string _MilkywayIntensity = "_MilkywayIntensity";
private const string _SunDirectionWS = "_SunDirectionWS";
private const string _StarTex = "_StarTex";
private const string _MoonTex = "_MoonTex";
private const string _MoonWorld2Local = "_MoonWorld2Obj";
private const string _MilkyWayWorld2Local = "_MilkyWayWorld2Local";
private SkyTimeData currentSkyTimeData;
private void OnEnable()
{
skyTimeDataController = GetComponent<SkyTimeDataController>();
}

void Start()
{

}

void Update()
{

currentSkyTimeData = skyTimeDataController.GetSkyTimeData(time);

ControllerSunAndMoonTransform();
SetProperties();
}


public void ControllerSunAndMoonTransform()
{
sunTransform.rotation = mainLight.transform.rotation;


skyboxMat.SetVector(_SunDirectionWS, sunTransform.forward);
}

void SetProperties()
{
skyboxMat.SetTexture(_StarTex, starTex);
skyboxMat.SetTexture(_MoonTex, moonTex);
skyboxMat.SetTexture(_SkyGradientTex, currentSkyTimeData.skyColorGradientTex);
skyboxMat.SetFloat(_StarIntensity, currentSkyTimeData.starIntensity);
skyboxMat.SetFloat(_MilkywayIntensity, currentSkyTimeData.milkywayIntensity);

skyboxMat.SetMatrix(_MoonWorld2Local, sunTransform.worldToLocalMatrix);
skyboxMat.SetMatrix(_MilkyWayWorld2Local, milkyWayTransform.worldToLocalMatrix);

}
}

于是就变成了这样,拖动时间条就可以看到天空变化
image.png

光源管理

主要问题在于昼夜交替时该如何处理,难道真要同时存在两个光源?硬切的话会显得有些违和。
目前没想到什么好的方法,姑且在昼夜交替时开启两个光源吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void ControlSunAndMoonTransform()
{
mainLight_Sun.transform.eulerAngles = new Vector3((time - 6)*180/12, 180, 0);

if(time >= 18)
mainLight_Moon.transform.eulerAngles = new Vector3((time - 18)*180/12, 180, 0);
else if(time >= 0)
mainLight_Moon.transform.eulerAngles = new Vector3((time)*180/12 + 90, 180, 0);

sunTransform.eulerAngles = mainLight_Sun.transform.eulerAngles;
moonTransform.eulerAngles = mainLight_Moon.transform.eulerAngles;

skyboxMat.SetVector(_SunDirectionWS, sunTransform.forward);
skyboxMat.SetVector(_MoonDirectionWS, moonTransform.forward);
}

然后在昼夜切换的时候让其中一个光源逐渐关闭就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void Update()
{
//......
time %= 24f;
if(!dayOrNightChanging)
{
if(Mathf.Abs(time - 6f) < 0.01f)
{
Debug.Log("Day");
StartCoroutine("ChangeToDay");
}
if(Mathf.Abs(time - 18f) < 0.01f)
{
Debug.Log("Night");
StartCoroutine("ChangeToNight");
}
}
//......
}

IEnumerator ChangeToNight()
{
dayOrNightChanging = true;
Light moon = mainLight_Sun;
Light sun = mainLight_Moon;
moon.enabled = true;
float updateTime = 0f;
while(updateTime <= 1)
{
updateTime += Time.deltaTime;
moon.intensity = Mathf.Lerp(moon.intensity, 0.7f, updateTime);
sun.intensity = Mathf.Lerp(sun.intensity, 0, updateTime);

yield return 0;
}
sun.enabled = false;
dayOrNightChanging = false;
}

IEnumerator ChangeToDay()
{
dayOrNightChanging = true;
Light moon = mainLight_Sun;
Light sun = mainLight_Moon;
sun.enabled = true;
float updateTime = 0f;
while(updateTime <= 1)
{
moon.intensity = Mathf.Lerp(moon.intensity, 0f, updateTime);
sun.intensity = Mathf.Lerp(sun.intensity, 1f, updateTime);
updateTime += Time.deltaTime;

yield return 0;
}
moon.enabled = false;
dayOrNightChanging = false;
}


最后奉上Github链接:https://github.com/FlowingCrescent/SimpleProceduralSkybox_URP

结语

天空盒这个坑很大,要做得好还得研究大气散射,星星和银河的方案也明显可以优化,虽然这次做得比较简单,但也学到了许多。


感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。